一个控制器(Controllers)处理请求并创建或准备响应 ,是请求范围。 换句话说,会为每个
request
创建一个新的实体。 一个控制器(Controller)可以生成响应或委托给视图。 创建一个控制器(Controller)只需要创建一个以
Controller
结尾的类。并放置于
grails-app/controllers
目录下。
默认的 URL
Mapping 设置确保控制器(Controllers)名字的第一个部分被映射到URI上
,每个在控制器(Controllers)中定义的操作(Action)被映射到控制器(Controller)名字URI中的URI。
创建控制器(Controller)
可以通过 create-controller
创建控制器(Controllers)。例如,你可以在Grails项目的根目录尝试运行下面命令:
grails create-controller book
这条命令将会在grails-app/controllers/BookController.groovy
路径下创建一个控制器(Controller):
class BookController { … }
BookController
默认被映射到 /book
URI(相对于你应用程序根目录).
create-controller
命令只不过是方便的工具,你同样可以使用你喜欢的文本编辑器或IDE更容易的创建控制器(Controller)
创建操作(Action)
一个控制器(Controllers)
可以拥有多个属性,每个属性都可以被分配一个代码块。所有这样的属性都被映射到URI:
class BookController {
def list = { // do controller logic
// create model
return model
}
}
默认情况下,由于上面示例属性名被命名为list
所以被映射到/book/list
URI。
默认Action
一个控制器(Controller)具有默认 URI概念,即被映射到
控制器(Controller)的根URI。默认情况下,默认的URI是/book
。 默认的URI通过下面的规则来规定:
- 如果只存在一个操作(Action), 控制器(Controller)默认的URI映射为这个。
- 如果定义了
index
操作(Action)用于处理请求,并且没有操作(Action)在 URI /book
中指定
- 作为选择,你还可以明确的通过
defaultAction
属性来设置。 property:
def defaultAction = "list"
可用的作用域
作用域本质上就是hash对象,它允许你存储变量。 下面的作用域在
控制器(Controller)中可以使用:
存取作用域
作用域可以通过上面的变量名与Groovy数组索引操作符结合来进行存取。甚至是Servlet
API提供的类,像 HttpServletRequest:
class BookController {
def find = {
def findBy = params["findBy"]
def appContext = request["foo"]
def loggedUser = session["logged_user"] }
}
你设置可以使用.操作符来存取作用域中的变量,这是语法更加清楚:
class BookController {
def find = {
def findBy = params.findBy
def appContext = request.foo
def loggedUser = session.logged_user }
}
这是Grails统一存取不同作用域的一种方式。
使用Flash作用域
Grails 支持 flash作用域的概念,它只用于临时存储用于这个请求到下个请求的属性,然后,这个属性就会被清除
对于重定向前直接设置消息是非常有用的,例如:
def delete = {
def b = Book.get( params.id )
if(!b) {
flash.message = "User not found for id ${params.id}"
redirect(action:list)
}
… // remaining code
}
Returning the Model
一个model本质上就是一个map,在视图渲染时使用。map中的keys转化为变量名,用于视图的获取。
第一种方式是明确的return一个model:
def show = {
[ book : Book.get( params.id ) ]
}
如果没有明确的 model被return,控制器(Controller)的属性将会被视为
model。 所以允许你这样编写代码:
class BookController {
List books
List authors
def list = {
books = Book.list()
authors = Author.list()
}
}
这可能由于实际上控制器(Controller)是
prototype(原型)范围。换句话说,每个请求都会创建一个新的控制器(Controller)。 否则,像上面的代码,就不会是线程安全的。
上面示例中,books
和 authors
属性在视图中都是可用的。
一个更高级的方式就是 return一个 Spring ModelAndView 类的实体:
import org.springframework.web.servlet.ModelAndViewdef index = {
def favoriteBooks = … // get some books just for the index page, perhaps your favorites
// forward to the list view to show them
return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
}
选择View
在之前的2个示例中,都没有指定哪个 view
用于渲染。因此,Grails怎么知道哪个 view被选取?答案在于规约。对于action:
class BookController {
def show = {
[ book : Book.get( params.id ) ]
}
}
Grails 会自动查找位于 grails-app/views/book/show.gsp
view
(事实上, Grails 会首先查找JSP,因为,Grails同样可以与 JSP一起使用).
假如,你想渲染其他view, render
方法在这里就能帮助你:
def show = {
def map = [ book : Book.get( params.id ) ]
render(view:"display", model:map)
}
这种情况下,Grails将会尝试渲染位于 grails-app/views/book/display.gsp
的view。注意,Grails自动描述位于book
文件夹中的
grails-app/views
路径位置的视图。很方便,但是,如果你拥有某些共享的视图需要存取,作为替代使用:
def show = {
def map = [ book : Book.get( params.id ) ]
render(view:"/shared/display", model:map)
}
在这种情况下,Grails将尝试渲染grails-app/views/shared/display.gsp
位置上的视图。
渲染响应
有时它很容易的渲染来自创建控制器小块文本或者代码的响应(通常使用Ajax应用程序)。因为,使用高度灵活的
render
方法:
上面的代码,在响应中写入 "Hello World!"文本, 其他的示例包括:
// write some markup
render {
for(b in books) {
div(id:b.id, b.title)
}
}
// render a specific view
render(view:'show')
// render a template for each item in a collection
render(template:'book_template', collection:Book.list())
// render some text with encoding and content type
render(text:"<xml>some xml</xml>",contentType:"text/xml",encoding:"UTF-8")
如果,你打算使用Groovy的MarkupBuilder来产生html,可以使用render来避免html元素与Grails标签之间的命名冲突。例如:
def login = {
StringWriter w = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(w)
builder.html{
head{
title 'Log in'
}
body{
h1 'Hello'
form{ }
}
}
def html = w.toString()
render html
}
实际上调用
form标签 (将返回一些文本,而忽略MarkupBuilder). 为了正确的输出 <form>元素,使用下面这些:
def login = {
// …
body{
h1 'Hello'
builder.form{ }
}
// …
}
Redirects
使用redirect方法,Actions(操作)可在所有的控制器(Controller)中重定向:
class OverviewController {
def login = {} def find = {
if(!session.user)
redirect(action:login)
…
}
}
redirect 方法内部使用HttpServletResonse对象的sendRedirect
方法。
redirect
方法可以选择如下用法之一:
- 同一个控制器(Controller)类中的其他闭包:
// 调用同一个类的login action
redirect(action:login)
- 一个控制器(Controller)和一个操作(Action)的名字:
// 重定向到home 控制器(Controller)的index action
redirect(controller:'home',action:'index')
// 明确的重定向到URI
redirect(uri:"/login.html")
// 重定向到一个URL
redirect(url:"http://grails.org")
使用方法的params
参数,参数可以选择性的从一个
action传递到下一个:
redirect(action:myaction, params:[myparam:"myvalue"])
通过 params动态属性,这些方法变得可用,同样也接受request参数。
如果指定一个名字与request参数的名字相同的参数,则 request参数被隐藏,控制器(Controller)参数被使用。
因为 params
对象也是一个
map,可以使用它把当前的request参数,从一个 action传递到下一个:
redirect(action:"next", params:params)
最后,你也可以在一个目标URI上包含一个片段(fragment):
redirect(controller: "test", action: "show", fragment: "profile")
将(依靠 URL mappings) 导航到/myapp/test/show#profile"。
h4. 链接
Actions同样可以被链接。链接允许model在一个操作(Action)到下一个操作(Action)中保留。例如下面调用first
action :
class ExampleChainController {
def first = {
chain(action:second,model:[one:1])
}
def second = {
chain(action:third,model:[two:2])
}
def third = {
[three:3])
}
}
model的结果:
通过chainModel
map,这个 model在chain中会被随后的
控制器(controller)操作(actions)存取.
这个动态属性只存在于随后调用chain
方法的操作(actions)中:
class ChainController { def nextInChain = {
def model = chainModel.myModel
…
}
}
Like the redirect
method you can
also pass parameters to the chain
method:
chain(action:"action1", model:[one:1], params:[myparam:"param1"])
通常,它用于拦截基于每个request(请求),session(会话)或应用程序状态的数据处理,这可以通过 action(操作)拦截器来实现。
目前有两种拦截器类型: before 和 after.
假如你的拦截器可能被用于更多的controller(控制器),
几乎肯定会写一个更好的 Filter(过滤器).
Filters(过滤器) 可以应用于多个controllers(控制器)或 URIs, 无需改变任何controller(控制器)逻辑.
Before 拦截器
beforeInterceptor
在action
(操作)被执行前进行数据处理拦截 . 假如它返回 false
,那么 ,被拦截的action (操作)将不会被执行.
拦截器可以像下面这样被定义为拦截一个controller(控制器)中所有的action (操作):
def beforeInterceptor = {
println "Tracing action ${actionUri}"
}
上面是在controller(控制器)定义主体内被声明. 它会在所有
action(操作)之前被执行,并且不会干扰数据处理. 一个普通的使用情形是为了验证:
def beforeInterceptor = [action:this.&auth,except:'login']
// defined as a regular method so its private
def auth() {
if(!session.user) {
redirect(action:'login')
return false
}
}
def login = {
// display login page
}
上面的代码定义了一个名为auth
的方法.
使用一个方法,是为了让它不会作为一个 action(操作)而暴露于外界(即. 它是private). 随后,beforeInterceptor
定义用于'except' login actions(操作)之外的所有 actions(操作)的拦截,并告知执行'auth' 方法.
'auth' 方法是使用Groovy的方法指针语法来引用 ,在方法内部,它自己会检测是否一个用户在session(会话)内,否则,重定向到
login action(操作) 并返回 false, 命令被拦截的actions(操作)不被执行 .
After 拦截器
为了定义一个在actions(操作)之后执行的拦截,可以使用afterInterceptor
属性:
def afterInterceptor = { model ->
println "Tracing action ${actionUri}"
}
after 拦截器把结果
model作为参数,所以,可以执行model或response的post操作.
after 拦截器 也可以在渲染之前修改Spring MVC ModelAndView对象. 在这种情况下, 上面的示例变成:
def afterInterceptor = { model, modelAndView ->
println "Current view is ${modelAndView.viewName}"
if(model.someVar) modelAndView.viewName = "/mycontroller/someotherview"
println "View is now ${modelAndView.viewName}"
}
通过当前action(操作),允许基于被返回的model改变视图.
注意,如果action(操作)被拦截调用redirect 或render, modelAndView
可能为null
.
拦截条件
Rails 用户非常熟悉验证示例 ,以及如何在'except'条件的使用下执行拦截
(拦截器在Rails中被称为'过滤器', 这个术语与Java领域中的servlet 过滤器术语有冲突):
def beforeInterceptor = [action:this.&auth,except:'login']
除了被指定的actions(操作),它执行所有actions(操作)的拦截.
一组actions(操作)列表同样可以像下面这样被定义:
def beforeInterceptor = [action:this.&auth,except:['login','register']]
其他被支持的条件是'only', 它只对被指定的actions(操作)执行拦截:
def beforeInterceptor = [action:this.&auth,only:['secure']]
数据绑定是"绑定"进入的请求参数到一个对象的属性或者一个完整对象图的行为. 数据绑定将处理所有来自请求参数必要的类型装换,典型的传送通过表单提交
, 始终是字符串,尽管Groovy或Java对象的属性可能不一定是.
Grails使用 Spring's底层的数据绑定能力来完成数据绑定.
绑定Request数据到Model上
这里有2种方式来绑定请求参数到domain类的属性上.
第一种涉及使用domain类的隐式构造函数:
def save = {
def b = new Book(params)
b.save()
}
这里的数据绑定发生在代码new Book(params)
内.通过传递
params
对象给domain类的构造函数, Grails 自动识别来自请求参数的绑定 . 因此,假如你有一个这样进入的请求 :
/book/save?title=The%20Stand&author=Stephen%20King
title
和author
请求参数将会自动
被设置到domain类上. 假如,你需要在一个已存在的实体上执行数据绑定,那么你可以使用 properties
属性:
def save = {
def b = Book.get(params.id)
b.properties = params
b.save()
}
这个和使用隐式构造函数是完全一样的效果.
数据绑定和单向关联
如果你有one-to-one
或 many-to-one
关联,你同样可以使用Grails的数据绑定能力更新这些关系. 例如,如果你有这样的请求参数:
Grails 将自动检测请求参数上的 .id
后缀,并查找给定id的
Author
实体 ,随后像这样进行数据绑定:
属于绑定与Many-ended关联
假如你有一个 one-to-many 或
many-to-many关联,依赖关联类型,有不同的方法用于数据绑定.
假如你有一个以Set
基本的关联 (默认用于hasMany
)
,那么简单的方式加入一个关联是简单的传送一组标识符列表. 考虑下面 <g:select>
示例的用法:
<g:select name="books"
from="${Book.list()}"
size="5" multiple="yes" optionKey="id"
value="${author?.books}" />
它生成一个选择框 ,允许你选择多个值.
在这种情况下,如果你提交表单,Grails将自动利用来自选择框的标识符加入 books
关联.
不过, 假如,你有一个更新关联对象的属性的方案,这个方法将不会工作.
作为替代,你需要使用下标操作符:
<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
不过, 如果,你想要更新在相同顺序中的渲染标记,对于基于Set
的关联是危险的
. 这是因为Set
没有顺序的概念, 所以,你引用的books0
和
books1
不能确保关联的顺序在服务器端的正确性,除非你自己应用明确排序
.
如果你使用基于List
的关联就不会存在这个问题
, 因为List
拥有确定的顺序并使供索引来引用. 这同样适用于基于 Map
的关联.
还要注意 ,假如你绑定的关联长度为,你引用的元素超出了关联的长度:
<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[2].title" value="Red Madder" />
随后, Grails 在确定的位置自动为你创建一个实体. 如果你"跳过"中间的某些元素
:
<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[5].title" value="Red Madder" />
随后,Grails会自动在中间创建实体 . 例如,如果关联的长度为2,在上面的情况下,Grails会创建4
个额外的实体.
数据绑定多个domain类
它可能通过来自
params对象来绑定多个domain对象.
例如,你有一个进入的请求:
/book/save?book.title=The%20Stand&author.name=Stephen%20King
需要注意的是,上面请求不同之处在于拥有 author.
前缀或 book
前缀.
这是用于分离哪个参数属于哪个类型. Grails的params
对象就像 多维 hash
,你可以索引来分离唯一的参数子集来绑定.
def b = new Book(params['book'])
注意,我们如何使用book.title
的第一圆点前面的前缀参数来隔离唯一的参数绑定.
我们同样可以这样来使用Author
domain类
:
def a = new Author(params['author'])
数据绑定与类型转换错误
有时,当执行数据绑定时,它可能不会将一种指定的String转换为指定的目标类型.
你会得到类型转换错误. Grails 会保留类型转换错误在Grails domain 类的 errors
属性中 . 例如这里:
class Book {
…
URL publisherURL
}
这里,我们有一个Book
domain 类 ,它使用Java的java.net.URL
来表示
URLs.现在,我们有一个像这样的请求参数:
/book/save?publisherURL=a-bad-url
在这种情况下,它不可能将 字符串a-bad-url
绑定到 publisherURL
属性上,一个类型匹配错误会发生. 你可以像这样来检查它们:
def b = new Book(params)if(b.hasErrors()) {
println "The value ${b.errors.getFieldError('publisherURL').rejectedValue} is not a valid URL!"
}
虽然,我们没有覆盖错误代码 (更多信息查看 Validation),
你需要的类型转换错误的错误消息在grails-app/i18n/messages.properties 内.
你可以使用像下面这样的普通错误消息来处理 :
typeMismatch.java.net.URL=The field {0} is not a valid URL
或更具体点:
typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL
数据绑定与安全关系
当批量更新来自请求参数的属性,你必须小心,避免客户端绑定恶意数据到 domain 类上,
并持久化到数据库.你可以使用下标操作符限制捆绑在某个给定domain类的属性:
def p = Person.get(1)p.properties['firstName','lastName'] = params
在这种情况下,只有firstName
和 lastName
属性将被捆绑.
另一种实现这个的方式是使用 domain类作为数据绑定目标,你可以使用Command
Objects. 另外还有一个更加灵活bindData 方法.
The bindData
方法具有同样的数据绑定能力,但,是对于任意的对象:
def p = new Person()
bindData(p, params)
当然,bindData
方法同样允许你排除某些你不想更新的参数:
def p = new Person()
bindData(p, params, [exclude:'dateOfBirth'])
或只包含某些属性:
def p = new Person()
bindData(p, params, [include:['firstName','lastName]])
使用render方法输出XML
Grails支持一些不同的方法来产生XML和JSON响应. 第一个是通过 render
方法.
render
方法可以传递一个代码块来实现XML中的标记生成器:
def list = {
def results = Book.list()
render(contentType:"text/xml") {
books {
for(b in results) {
book(title:b.title)
}
}
}
}
这段代码的结果会像这样:
<books>
<book title="The Stand" />
<book title="The Shining" />
</books>
注意,你必须小心的是避免使用标记生成器带来的命名冲突. 例如,这段代码会产生一个错误:
def list = {
def books = Book.list() // naming conflict here
render(contentType:"text/xml") {
books {
for(b in results) {
book(title:b.title)
}
}
}
}
问题在于,这里的局部变量 books
,
Groovy会把它当做一个方法来调用.
使用render方法输出JSON
render
同样被用于输出JSON:
def list = {
def results = Book.list()
render(contentType:"text/json") {
books {
for(b in results) {
book(title:b.title)
}
}
}
}
在这种情况下,结果大致相同:
[
{title:"The Stand"},
{title:"The Shining"}
]
同样的命名冲突危险适用于JSON生成器.
自动XML列集(Marshalling)
(译者注:在此附上对于列集(Marshalling)解释:对函数参数进行打包处理得过程,因为指针等数据,必须通过一定得转换,才能被另一组件所理解。可以说列集(Marshalling)是一种数据格式的转换方法。)
Grails同样支持自动列集(Marshalling) domain类 为XML,通过特定的转换器.
首先,导入grails.converters
类包到你的controller(控制器):
import grails.converters.*
现在,你可以使用下列高度易读的语法来自动转换domain类为XML:
render Book.list() as XML
输出结果看上去像下面这样:
<?xml version="1.0" encoding="ISO-8859-1"?>
<list>
<book id="1">
<author>Stephen King</author>
<title>The Stand</title>
</book>
<book id="2">
<author>Stephen King</author>
<title>The Shining</title>
</book>
</list>
一个使用转换器的替代方法是使用Grails的codecs 特性. codecs特性提供了 encodeAsXML 和 encodeAsJSON方法:
def xml = Book.list().encodeAsXML()
render xml
更多的XML 列集(Marshalling)信息见REST
自动JSON列集(Marshalling)
Grails同样支持自动列集(Marshalling)为JSON通过同样的机制. 简单替代XML
为JSON
:
render Book.list() as JSON
输出结果看上去像下面这样:
[
{"id":1,
"class":"Book",
"author":"Stephen King",
"title":"The Stand"},
{"id":2,
"class":"Book",
"author":"Stephen King",
"releaseDate":new Date(1194127343161),
"title":"The Shining"}
]
作为替代,你可以使用encodeAsJSON
达到相同的效果.
文件上传程序
Grails通过Spring的 MultipartHttpServletRequest 接口来支持文件上传.
上传文件的第一步就是像下面这样创建一个multipart form:
Upload Form: <br />
<g:form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="myFile" />
<input type="submit" />
</g:form>
这里有一些方法来处理文件上传. 第一种方法是直接与Spring的MultipartFile 实体:
def upload = {
def f = request.getFile('myFile')
if(!f.empty) {
f.transferTo( new File('/some/local/dir/myfile.txt') )
response.sendError(200,'Done');
}
else {
flash.message = 'file cannot be empty'
render(view:'uploadForm')
}
}
这显然很方便,通过MultipartFile 接口可以直接获得一个InputStream,用来转移到其他目的地和操纵文件等等.
通过数据绑定上传文件
文件上传同样可以通过数据绑定来完成。例如,假定你有一个像下面这样Image
domain类:
class Image {
byte[] myFile
}
现在,假如你创建一个image并像下面这个示例一样传入 params
对象,Grails将自动把文件的内容当作一个byte绑定到myFile
属性:
def img = new Image(params)
它同样可以设置文件的内容为一个string,通过改变image的myFile
属性类型为一个String类型:
class Image {
String myFile
}
Grails控制器(controllers)支持命令对象概念.一个命令对象类似于Struts中的一个formbean,它们在当你想要写入属性子集来更新一个domain类情形时是非常有用的
. 或在没有domain类需要的相互作用,但必须使用
data
binding
和
validation
特性 .
声明命令对象
命令对象通常作为一个控制器直接声明在控制器(controller)类定义下的同一个源文件中.
例如:
class UserController {
…
}
class LoginCommand {
String username
String password
static constraints = {
username(blank:false, minSize:6)
password(blank:false, minSize:6)
}
}
上面的示例证明你可以提供 约束给命令对象,就象你在domain 类中的用法一样.
使用命令对象
为了使用命令对象,控制器可以随意指定任何数目的命令对象参数。必须提供参数的类型以至于Grails能知道什么样的对象被创建,写入和验证.
在控制器(controller)的操作被执行之前,Grails将自动创建一个命令对象类的实体,用相应名字的请求参数写入到命令对象属性,
并且命令对象将被验证,例如:
class LoginController {
def login = { LoginCommand cmd ->
if(cmd.hasErrors()) {
redirect(action:'loginForm')
}
else {
// do something else
}
}
}
命令对象与依赖注入
命令对象可以参与依赖注入。这有利于一些定制的验证逻辑与Grails的services的结合。
:
class LoginCommand {
def loginService String username
String password
static constraints = {
username(validator: { val, obj ->
obj.loginService.canLogin(obj.username, obj.password)
})
}
}
上面示例,命令对象与一个来自Spring的 ApplicationContext
注入名字bean结合.
Grails 已经内置支持处理重复表单提交, 通过使用"同步令牌模式". 首先,你得在
form
标签上定义一个令牌:
<g:form useToken="true" ...>
随后,在你的控制器(controller)代码中使用 withForm
方法来处理有效和无效的请求:
withForm {
// good request
}.invalidToken {
// bad request
}
如果你只提供了 withForm
方法而没有链接 invalidToken
方法,那么,默认情况下,Grails 将会无效的令牌存储在flash.invalidToken
变量中
并导航请求回到原始页面. 这可以在页面中检测到:
<g:if test="${flash.invalidToken}">
Don't click the button twice!
</g:if>
withForm
标签利用了session
,因此,如果在群集中使用,要求会话密切关联.
Groovy Servers Pages (或者简写为
GSP)Grails的视图技术。它被设计成像ASP和JSP这样被使用者熟悉的技术,但更加灵活和直观.
GSP存在于Grails的grails-app/views
目录中,他们通常会自动渲染(通过规约),或者像这样通过render方法:
GSP使典型的混合标记和GSP标签,辅助页面渲染.
虽然,它可能会在你的GSP页面中内置Groovy逻辑,Although
it is possible to have Groovy logic embedded in your GSP and doing this
will be covered in this document the practice is strongly discouraged.
Mixing mark-up and code is a bad thing and
most GSP pages contain no code and needn't do so.
一个GPS通常拥有一个"model",它是变量集被用于视图渲染。通过一个控制器model被传递到GSP视图。例如,考虑下列控制器的操作:
def show = {
[book: Book.get(params.id)]
}
这个操作将查找一个book
实体,并创建一个包含关键字为Book
的model,这个关键字可在随后的GSP视图中应用:
在下一节,我们将通过GSP基础知识让你知道它能做什么。首先,我们将涵盖基础语法,对于JSP和ASP用户是非常熟悉的.
GSP支持使用<% %>
来嵌入Groovy代码(这是不推荐的):
<html>
<body>
<% out << "Hello GSP!" %>
</body>
</html>
同样,你可以使用<%= %>
语法来输出值:
<html>
<body>
<%="Hello GSP!" %>
</body>
</html>
GSP同样支持服务器端JSP样式注释,像下列示例显示的这样:
<html>
<body>
<%-- This is my comment --%>
<%="Hello GSP!" %>
</body>
</html>
在
<% %>
中你当然可以声明变量:
然后,在页面中的之后部分可以重复使用 :
然而, 在GSP中存在着一些预先定义的变量,包括:
使用
<% %>
语法,你当然可以使用这样的语法进行嵌套循环等等操作:
<html>
<body>
<% [1,2,3,4].each { num -> %>
<p><%="Hello ${num}!" %></p>
<%}%>
</body>
</html>
同样可以分支逻辑:
<html>
<body>
<% if(params.hello == 'true' )%>
<%="Hello!"%>
<% else %>
<%="Goodbye!"%>
</body>
</html>
GSP同样支持少许的JSP样式页面指令.
import指令允许在页面中导入类。然而,它却很少被使用,因为Groovy缺省导入和GSP 标签:
<%@ page import="java.awt.*" %>
GSP同样支持contentType@ 指令:
<%@ page contentType="text/json" %>
contentType@指令允许GSP使用其他的格式来渲染.
尽管GSP也支持
<%= %>
语法,而且很早就介绍过,但在实际当中却很少应用,因为此用法主要是为ASP和 、JSP开发者所保留的。 而GSP的表达式跟JSP
EL表达式很相似的,跟Groovy GString的
${expr}
用法也很像:
<html>
<body>
Hello ${params.name}
</body>
</html>
尽管如此,跟JSP EL不同的是, 你可以在${..}
括号中使用Groovy表达式.${..}
中的变量缺省情况下是非转义,
因此变量的任何HTML字符串内容被直接输出到页面,要减少这种Cross-site-scripting (XSS)攻击的风险, 你可以设置grails-app/conf/Config.groovy
中的grails.views.default.codec
为HTML转化方式:
grails.views.default.codec='html'
其他可选的值是'none' (缺省值)和'base64'.
现在,JSP遗传下来的缺点已经被取消,下面的章节将涵盖GSP的内置标签,它是定义GSP页面最有利的方法.
标签库
部分涵盖怎么添加你自己的定制标签库.
所有GSP内置标签以前缀g:开始。 不像JSP,你不需要指定任何标签库的导入.假如,一个标签以g:
开始,它被自动认为是一个GSP标签.一个GPS标签的示例看起来像这样:
GSP标签同样可以拥有主体,像这样:
<g:example>
Hello world
</g:example>
表达式被传递给GSP标签属性,假如没有使用表达式,将被认为是一个String值:
<g:example attr="${new Date()}">
Hello world
</g:example>
Maps同样能被传递给GSP标签属性,通常使用一个命名参数样式语法:
<g:example attr="${new Date()}" attr2="[one:1, two:2, three:3]">
Hello world
</g:example>
注意,对于String类型属性值,你必须使用单引号:
<g:example attr="${new Date()}" attr2="[one:'one', two:'two']">
Hello world
</g:example>
在介绍完基本的语法之后,下面我们来讲解Grails中默认提供的标签.
变量可以在GSP中使用
set
标签来定义:
<g:set var="now" value="${new Date()}" />
这里, 我们给GSP表达式结果赋予了一个名为now
的变量
(简单的构建一个新的 java.util.Date 实体)。 你也可以在<g:set>
主体中定义一个变量:
<g:set var="myHTML">
Some re-usable code on: ${new Date()}
</g:set>
变量同样可以被放置于下列的范围内:
page
- 当前页面范围 (默认)
request
- 当前请求范围
flash
- flash 作用域,因此它可以在下一次请求中有效
session
- 用户session范围
application
- 全局范围.
选择变量被放入的范围可以使用scope
属性:
<g:set var="now" value="${new Date()}" scope="request" />
GSP同样支持迭代逻辑标签,逻辑上通过使用
if
, ,
else
和
elseif
来支持典型的分支情形。 :
<g:if test="${session.role == 'admin'}">
<%-- show administrative functions --%>
</g:if>
<g:else>
<%-- show basic functions --%>
</g:else>
GSP用each each和while标签来处理迭代:
<g:each in="${[1,2,3]}" var="num">
<p>Number ${num}</p>
</g:each>
<g:set var="num" value="${1}" />
<g:while test="${num < 5 }">
<p>Number ${num++}</p>
</g:while>
假如你拥有对象集合,你经常需要使用一些方法来排序和过滤他们。 GSP支持
findAll
和
grep
来做这些工作:
Stephen King's Books:
<g:findAll in="${books}" expr="it.author == 'Stephen King'">
<p>Title: ${it.title}</p>
</g:findAll>
expr
属性包含了一个Groovy表达式,它可以被当作一个过滤器来使用。
谈到过滤器,grep标签通过类来完成与过滤器类似的工作:
<g:grep in="${books}" filter="NonFictionBooks.class">
<p>Title: ${it.title}</p>
</g:grep>
或者使用一个正则表达式:
<g:grep in="${books.title}" filter="~/.*?Groovy.*?/">
<p>Title: ${it}</p>
</g:grep>
上面的示例同样有趣,因为它使用了GPath.Groovy的GPath等同与XPath语言。实际上books集合是books
集合的实体。
不过,假设每个books
拥有一个title
,你可以使用表达式books.title
来获取Book
titles的list!
GSP还拥有特有的标签来帮助你管理连接到控制器和操作.
link
标签允许你指定控制器和操作配对的名字,并基于
URL
Mappings
映射来自动完成连接。即使你去改变!一些
link
的示例如下:
<g:link action="show" id="1">Book 1</g:link>
<g:link action="show" id="${currentBook.id}">${currentBook.name}</g:link>
<g:link controller="book">Book Home</g:link>
<g:link controller="book" action="list">Book List</g:link>
<g:link url="[action:'list',controller:'book']">Book List</g:link>
<g:link action="list" params="[sort:'title',order:'asc',author:currentBook.author]">
Book List
</g:link>
表单基础
GSP支持许多不同标签来帮助处理HTML表单和字段,最基础的是form标签, form标签是一个控制器/操作所理解的正规的HTML表单标签版本。
url
属性允许你指定映射到哪个控制器和操作:
<g:form name="myForm" url="[controller:'book',action:'list']">...</g:form>
我们创建个名为myForm
的表单,它被提交到 BookController
的list
操作。除此之外,适用于所有不同的HTML属性.
表单字段
同构造简单的表单一样,GSP支持如下不同字段类型的定制:
上面的每一个都允许GSP表达式作为值:
<g:textField name="myField" value="${myValue}" />
GSP同样包含上面标签的扩张助手版本, 比如radioGroup (创建一组radio标签), localeSelect, currencySelect
和timeZoneSelect(选择各自的地区区域,
货币 和时间区域). .
多样的提交按钮
处理多样的提交按钮这样由来已久的问题,同样可以通过Grails的actionSubmit
标签优雅的处理。它就像一个正规提交,但是,允许你指定一个可选的操作来提交:
<g:actionSubmit value="Some update label" action="update" />
GSP标签和其他标签技术一个主要不同在于,来自
controllers(控制器)
,
标签库
或者GSP 视图中的GPS标签可以被当作任意的正规标签或者当作方法被调用.
来自GSPs中的标签当作方法调用
当作为方法被调用时,标签的返回值被当作String实体直接被写入响应中。 因此,示例中的createLinkTo能等同的看做方法调用:
Static Resource: ${createLinkTo(dir:"images", file:"logo.jpg")}
当你必须在一个属性内使用一个标签时是特别有用的:
<img src="${createLinkTo(dir:'images', file:'logo.jpg')}" />
I在视图技术中,标签内嵌套标签的特性是不被支持的,这样变得十分混乱,往往使得像Dreamweaver这样WYSWIG的工具产生不利的效果以至于在渲染标签时:
<img src="<g:createLinkTo dir="images" file="logo.jpg" />" />
来自控制器(Controllers)和标签库的标签作为方法调用
你同样可以调用来自控制器和标签库的标签。标签可以不需要内部默认的g:
namespace前缀来调用,并返回String结果:
def imageLocation = createLinkTo(dir:"images", file:"logo.jpg")
然而,你同样可以用命名空间前缀来避免命名冲突:
def imageLocation = g.createLinkTo(dir:"images", file:"logo.jpg")
假如你有一个自定义命名空间,,你可以使用它的前缀来替换(例如,使用
FCK
Editor plugin:
def editor = fck.editor()
除了views之外, Grails还有模板的概念. 模板有利于分隔出你的视图在可维护的块中,并与
Layouts
结合提供一个高度可重用机制来构建视图.
模板基础
Grails使用在一个视图名字前放置一个下划线来标识为一个模板的规约。 例如,你可能有个位于grails-app/views/book/_bookTemplate.gsp
的模板处理渲染Books:
<div class="book" id="${book?.id}">
<div>Title: ${book?.title}</div>
<div>Author: ${book?.author?.name}</div>
</div>
为了渲染来自grails-app/views/book
视图中的一个模板,你可以使用render标签:
<g:render template="bookTemplate" model="[book:myBook]" />
注意,我们是怎么样使用render标签的model属性来使用传入的一个model
。
假如,你有多个Book
实体,你同样可以使用render标签为每个Book
渲染模板 :
<g:render template="bookTemplate" var="book" collection="${bookList}" />
共享模板
在早先的示例中,我们有一个特定于BookController
模板,它的视图位于grails-app/views/book
.然而,你可能想横跨你的应用来共享模板。
在这种情况下,你可以把他们放置于grails-app/views视图根目录或者位于这个位置的任何子目录,然后在模板属性在模板名字之前使用一个 /
来指明相对模板路径
.例如,假如你有个名为grails-app/views/shared/_mySharedTemplate.gsp
模板,
你可以像下面这样引用它:
<g:render template="/shared/mySharedTemplate" />
你也可以使用这个技术从任何视图或控制器(Controllers)来引用任何目录下的模板:
<g:render template="/book/bookTemplate" model="[book:myBook]" />
模板命名空间
因为模板使用如此频繁,它有一个模板命名空间, 名为tmpl
, 他使模板的使用变得容易. 考虑下面例子的使用模式:
<g:render template="bookTemplate" model="[book:myBook]" />
这个想下面这样通过tmpl
命名空间表示
:
<tmpl:bookTemplate book="${myBook}" />
在控制器(Controllers)和标签库中的模板
你同样可以使用控制器 render方法渲染模板控制器中,它对Ajax引用很有用:
def show = {
def b = Book.get(params.id)
render(template:"bookTemplate", model:[book:b])
}
在控制器(controller)中的render
方法最普通的行为是直接写入响应。 假如,你需要获得模板作为一个String的结果作为替代,你可以使用render标签:
def show = {
def b = Book.get(params.id)
String content = g.render(template:"bookTemplate", model:[book:b])
render content
}
注意, g.
命名空间的用法,它告诉Grails我们想使用标签作为方法调用来代替render
方法.
创建布局
Grails利用了Sitemesh,一个装饰引擎,来支持视图布局。
布局位于grails-app/views/layouts
目录中。一个典型的布局如下:
<html>
<head>
<title><g:layoutTitle default="An example decorator" /></title>
<g:layoutHead />
</head>
<body onload="${pageProperty(name:'body.onload')}">
<div class="menu"></menu>
<div class="body">
<g:layoutBody />
</div>
</div>
</body>
</html>
关键的元素是layoutHead, layoutTitle和layoutBody标签的用法,这里是他们所做的:
layoutTitle
- 输出目标页面的title
layoutHead
- 输出目标页面head标签内容
layoutBody
- 输出目标页面body标签内容
上面的示例同样表明pageProperty tag
可被用于检查和返回目标页面的外观.
启用布局
这里有一些方法来启用一个布局.简单的在视图中添加meta标签:
<html>
<head>
<title>An Example Page</title>
<meta name="layout" content="main"></meta>
</head>
<body>This is my content!</body>
</html>
在这种情况下,一个名为grails-app/views/layouts/main.gsp
将被用于布局这个页面。假如,我们使用来自早前部分的布局,输出看上去像下列这样:
<html>
<head>
<title>An Example Page</title>
</head>
<body onload="">
<div class="menu"></div>
<div class="body">
This is my content!
</div>
</body>
</html>
在控制器(Controller)中指定布局
另一种用于指定布局的方式是通过在控制器(controller)中为 "layout"属性指定布局的名字, 假如你有个这样的控制器(controller):
class BookController {
static layout = 'customer' def list = { … }
}
你可以创建一个grails-app/views/layouts/customer.gsp
布局,应用于所有
BookController
中委派的视图
. "layout"属性值可能包含相对于grails-app/views/layouts/
目录的路径结构
. 例如:
class BookController {
static layout = 'custom/customer' def list = { … }
}
视图的显然可通过
grails-app/views/layouts/custom/customer.gsp
模板.
布局规约
第二种关联布局的方法是使用"布局规约",假如你有个这样的控制器:
class BookController {
def list = { … }
}
你可以创建一个名为grails-app/views/layouts/book.gsp
的布局,根据规约,它将被应用于BookController
的所有视图中。
换句话说,你可以创建一个名为grails-app/views/layouts/book/list.gsp
的布局,它将只被应用于BookController
中的list
操作,
如果你同时使用了以上提到的两种布局的话,那当list操作被执行的时候,那么操作将根据优先级的顺序来使用布局.
内联布局
通过applyLayout标签Grails同样支持Sitemesh的内联布局概念。 applyLayout
标签可以被用于应用一个布局到一个模板,URL或者内容的任意部分。
事实上,通过"decorating"你的模板允许你更进一步的积木化你的视图结构.
一些使用示例如下:
<g:applyLayout name="myLayout" template="bookTemplate" collection="${books}" />
<g:applyLayout name="myLayout" url="http://www.google.com" />
<g:applyLayout name="myLayout">
The content to apply a layout to
</g:applyLayout>
Server-Side包含
当 applyLayout标签被以用于引用布局外内容 applying layouts to
, 假如你想简单的在当前页面包含外部内容,你可以使用
include:
<g:include controller="book" action="list"></g:include>
你甚至可以结合 include 标签和 applyLayout 标签
来添加灵活性:
<g:applyLayout name="myLayout">
<g:include controller="book" action="list"></g:include>
</g:applyLayout>
最后,你也可以在控制器(controller)或标签库把include标签作为方法调用
:
def content = include(controller:"book", action:"list")
最后的内容有
include标签的返回值提供
.
虽然,这对于装饰全部页面非常有用,有时,你需要装饰站点的部分独自的页面。为了实现这个可以使用内容块. 在开始时,你需要使用
<content>
标签分隔装饰页面
:
<content tag="navbar">
… draw the navbar here…
</content>
<content tag="header">
… draw the header here…
</content>
<content tag="footer">
… draw the footer here…
</content>
<content tag="body">
… draw the body here…
</content>
随后,在布局内部,你可以引用这些组件并为每个引用单个布局:
<html>
<body>
<div id="header">
<g:applyLayout name="headerLayout"><g:pageProperty name="page.header"></g:applyLayout>
</div>
<div id="nav">
<g:applyLayout name="navLayout"><g:pageProperty name="page.navbar"></g:applyLayout>
</div>
<div id="body">
<g:applyLayout name="bodyLayout"><g:pageProperty name="page.body"></g:applyLayout>
</div>
<div id="footer">
<g:applyLayout name="footerLayout"><g:pageProperty name="page.footer"></g:applyLayout>
</div>
</body>
</html>
像
Java
Server Pages
JSP) 一样,GSP支持定制tag库的概念.不同于JSP,Grails标签库机制是简单的,优雅的,在运行时完全可重载的.
创建一个标签库是相当简单的,创建一个以规约TagLib
结尾的一个Groovy类,并把它放置于grails-app/taglib
目录里:
现在,为了创建一个标签,简单的创建属性并赋值一个带有两个参数的代码块:标签属性和主体内容:
class SimpleTagLib {
def simple = { attrs, body -> }
}
attrs
属性是一个简单的标签属性map,同时body
是另一可调用的代码块,它返回主体内容:
class SimpleTagLib {
def emoticon = { attrs, body ->
out << body() << attrs.happy == 'true' ? " :-)" : " :-("
}
}
正如以上所显示的,这里有个隐式的out
变量,它引用了输出Writer
,可以用来附加内容到响应中.
然后,你可以在你的GSP内简单的引用这个标签而不需要任何导入:
<g:emoticon happy="true">Hi John</g:emoticon>
在标签库的作用域中包含了一些预先定义好的变量:
作为演示,早先的示例只不过是写了个没有主体只有输出内容的简单标签。另一个示例是一个
dateFormat
样式标签:
def dateFormat = { attrs, body ->
out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
}
上面使用了Java的SimpleDateFormat
类来格式化一个date,然后把它写入响应。随后,这个标签能像下列这样在GSP中使用:
<g:dateFormat format="dd-MM-yyyy" date="${new Date()}" />
有时。你需要用简单的标签把HTML标签(mark-up)写入到响应中。一个方法是直接嵌套内容:
def formatBook = { attrs, body ->
out << "<div id="${attrs.book.id}">"
out << "Title : ${attrs.book.title}"
out << "</div>"
}
虽然,这个方法可能很诱人,但不是非常的简洁。一个更好的方法将是复用render标签:
def formatBook = { attrs, body ->
out << render(template:"bookTemplate", model:[book:attrs.book])
}
然后,这个单独的GSP模板做了实际的渲染工作.
一旦一组条件满足,你同样可以在标签的主体中创建仅仅用来输出的逻辑标签。一个这样的例子可能是一组安全标签:
def isAdmin = { attrs, body ->
def user = attrs['user']
if(user != null && checkUserPrivs(user)) {
out << body()
}
}
上面的标签检查用户是否为管理人员,如果他/她有正确设置的访问权限只输出主体内容:
<g:isAdmin user="${myUser}">
// some restricted content
</g:isAdmin>
迭代标签同样普通,因为你可以多次调用主体:
def repeat = { attrs, body ->
attrs.times?.toInteger().times { num ->
out << body(num)
}
}
在这个示例中,我们检查一个times
属性,假如存在,把它转换为一个数字,然后使用Groovy的times
方法:
<g:repeat times="3">
<p>Repeat this 3 times! Current repeat = ${it}</p>
</g:repeat>
注意,我们是怎么样在这个示例中使用隐式的it
变量来引用当前的数字。这个过程是因为在迭代内部我们调用了传递进入当前值的主体:
那个值然后被作为默认的it
变量传递给标签,然而,假如你有嵌套标签便会导致冲突,因此,你将可能替换主体使用的变量名:
def repeat = { attrs, body ->
def var = attrs.var ? attrs.var : "num"
attrs.times?.toInteger().times { num ->
out << body((var):num)
}
}
这里,我们检查是否存在一个var
属性,如果存在的话,将其作为body调用的参数:
注意,变量名围绕的圆括号的使用.假如你省略,Groovy会认为你使用了一个String关键字,而不是引用这个变量它自己.
现在,我们可以改变这个标签的使用方法,如下:
<g:repeat times="3" var="j">
<p>Repeat this 3 times! Current repeat = ${j}</p>
</g:repeat>
注意,我们是怎么样使用var
属性来定义j
变量名,随后,我们可能在标签主体类引用这个变量.
默认情况下,标签被添加到默认的Grails命名空间,并在GSP页面中和
g:
前缀一起使用。然而,你可以指定一个不同的命名空间,通过在你的
TagLib
类中添加一个静态属性:
class SimpleTagLib {
static namespace = "my"
def example = { attrs ->
…
}
}
这里,我们指定了一个命名空间
my
,因此,稍后在GPS页面中标签库中的标签引用会像这样:
<my:example name="..." />
前缀和静态的命名空间
属性值一样.命名空间对于插件特别有用.
命名空间内的标签可以作为方法调用,使用命名空间作为前缀来执行方法调用:
out << my.example(name:"foo")
可用于GSP,控制器或者标签库.
除了GSP提供的简单标签库机制, 你也可以在GSP中使用JSP标签.通过taglib
指令来简单声明你需要的JSP标签:
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
随后,你可以想任何其他标签一样来使用它:
<fmt:formatNumber value="${10}" pattern=".00"/>
额外的好处是,你可以把JSP标签当方法调用
:
${fmt.formatNumber(value:10, pattern:".00")}
到目前为止,贯穿整个文档用于URLs的规约默认为
/controller/action/id
. 然而,这个规约不是硬性的写入Grails中,实际上,它是通过一个位于
grails-app/conf/UrlMappings.groovy
的URL映射类所控制.
UrlMappings
类包含一个名为mappings
单一属性,并被赋予一个代码块:
class UrlMappings {
static mappings = {
}
}
为了创建简单的映射,只需简单的使用相对URL作为方法名,并指定控制器和操作的命名参数来映射:
"/product"(controller:"product", action:"list")
在这种情况下,我们建立URL/product
到ProductController
的list
操作的映射。
你当然可以省略操作定义,来映射控制器默认的操作:
"/product"(controller:"product")
一个可选的语法是把在块中被赋值的控制器和操作传递给方法:
"/product" {
controller = "product"
action = "list"
}
你使用哪一个句法很大程度上依赖于个人偏好.
简单变量
早前的部分说明,怎样使用具体的"标记"来映射普通的URLs。在URL映射里讲过,标记是在每个斜线(/)字符之间的顺序字符。 一个具体的标记就像/product
这样被良好定义。
然而,很多情况下,标记的值直到运行时才知道是什么。在这种情况下,你可以在URL中使用变量占位符,例如:
static mappings = {
"/product/$id"(controller:"product")
}
在这种情况下,通过嵌入一个$id变量作为第2个标记,Grails将自动映射第2个标记到一个名为id
的参数(通过params对象得到).
例如给定的URL/product/MacBook
,下面的代码将渲染"MacBook"到响应中:
class ProductController {
def index = { render params.id }
}
当然你可以构建更多复杂的映射示例。例如传统的blog URL格式将被映射成下面这样:
static mappings = {
"/$blog/$year/$month/$day/$id"(controller:"blog", action:"show")
}
上面的映射允许你这样:
/graemerocher/2007/01/10/my_funky_blog_entry
在URL里单独的标记将再次被映射到带有year
, month
,
day
, id
等等可用值的 params
对象中.
动态控制器(Controller)和操作(Action)名
变量同样可以被用于动态构造控制器和操作名。实际上,默认的Grails URL映射使用这样的技术:
static mappings = {
"/$controller/$action?/$id?"()
}
这里,控制器(controller)名,操作(action)名和id名,隐式的从嵌入在URL中的controller
,
action
和id
中获得:
static mappings = {
"/$controller" {
action = { params.goHere }
}
}
可选的变量
默认映射另一个特性就是能够在一个变量的末尾附加一个?
,使它成为一个可选的标记。这个技术更进一步的示例能够运用于blog URL映射,使它具有更灵活性的连接 :
static mappings = {
"/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}
下列URLs的所有映射将与放置于params对象中的唯一关联的参数匹配:
/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher
任意变量
你同样可以传递来自于URL映射的任意参数给控制器,把他们设置在块内传递给这个映射:
"/holiday/win" {
id = "Marrakech"
year = 2007
}
在这个params对象得到的这个变量将被传递给这个控制器.
动态解析变量
硬编码任意变量是有用的,但是,有时你需要基于运行时因素来计算变量名。这个同样可能通过给变量名分配一个块:
"/holiday/win" {
id = { params.id }
isEligible = { session.user != null } // must be logged in
}
上述情况,当URL实际被匹配,块中的代码将被解析,因此可以被用于结合所有种类的逻辑处理.
如果你想决定一个URL一个view,而无需涉及一个控制器或者操作,你也可以这样做。 例如,如果你想映射根URL
/
到一个位于
grails-app/views/index.gsp
的GSP,你可以这样使用:
static mappings = {
"/"(view:"/index") // map the root URL
}
换句话说,假如你需要一个具体给定的控制器(Controller)中的一个视图,你可以这样使用:
static mappings = {
"/help"(controller:"site",view:"help") // to a view for a controller
}
Grails同样允许你映射一个HTTP响应代码到控制器,操作或视图。所有你需要做的是使用一个方法名来匹配你所感兴趣的响应代码:
static mappings = {
"500"(controller:"errors", action:"serverError")
"404"(controller:"errors", action:"notFound")
"403"(controller:"errors", action:"forbidden")
}
或者换句话说,假如你只不过想提供定制的错误页面:
static mappings = {
"500"(view:"/errors/serverError")
"404"(view:"/errors/notFound")
"403"(view:"/errors/forbidden")
}
URL映射同样可以配置成基于HTTP 方法 (GET, POST, PUT or DELETE)的map。这个对于RESTful
APIs和基于HTTP方法的约束映射是非常有用的.
作为一个示例,下面的映射为ProductController
URL提供一个RESTful
API URL映射:
static mappings = {
"/product/$id"(controller:"product"){
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}
}
Grails的URL映射机制同样支持通配符映射。例如,考虑下面的映射:
static mappings = {
"/images/*.jpg"(controller:"image")
}
这个映射将匹配所有images路径下像/image/logo.jpg
这样的jpg。当然你可以通过一个变量来达到同样的效果:
static mappings = {
"/images/$name.jpg"(controller:"image")
}
然而,你可以使用双通配符来匹配多于一个层次之外的:
static mappings = {
"/images/**.jpg"(controller:"image")
}
这样的话,这个映射将不但匹配/image/logo.jpg
而且匹配/image/other/logo.jpg
。更好的是你可以使用一个双通配符变量:
static mappings = {
// will match /image/logo.jpg and /image/other/logo.jpg
"/images/$name**.jpg"(controller:"image")
}
这样的话,它将储存路径,从params 对象获得命名参数里的name
通配符 :
def name = params.name
println name // prints "logo" or "other/logo"
如果你使用通配符URL mappings,那么你可以排除某些来自Grails的URL mapping进程
URIs. 实现这个你可以在UrlMappings.groovy
类中设置excludes
:
class UrlMappings = {
static excludes = ["/images/**", "/css/**"]
static mappings = {
…
}
}
这样,Grails不为匹配任何以
/images
或 /css
开头的URLs.
URL映射另一个重要的特性是自动定制
link
标签的行为。以便改变这个映射而不需要改变所有的连接.
通过一个URL重写技术做到这点,从URL映射反转连接设计:
static mappings = {
"/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}
如果,你像下列一样使用连接标签:
<g:link controller="blog" action="show" params="[blog:'fred', year:2007]">My Blog</g:link>
<g:link controller="blog" action="show" params="[blog:'fred', year:2007, month:10]">My Blog - October 2007 Posts</g:link>
Grails将自动重写URL通过适当的格式:
<a href="/fred/2007">My Blog</a>
<a href="/fred/2007/10">My Blog - October 2007 Posts</a>
URL映射同样支持Grails统一
验证规约
机制, 它允许你更进一步"约束"一个URL是怎么被匹配的。例如,如果我们回到早前的blog示例代码,这个映射当前看上去会像这样 :
static mappings = {
"/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}
允许URLs像这样:
/graemerocher/2007/01/10/my_funky_blog_entry
不过,它也允许这样:
/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry
当它强迫你在控制器代码中做一些聪明的语法分析时会有问题。幸运的是,URL映射能进一步的约束验证URL标记:
"/$blog/$year?/$month?/$day?/$id?" {
controller = "blog"
action = "show"
constraints {
year(matches:/d{4}/)
month(matches:/d{2}/)
day(matches:/d{2}/)
}
}
在这种情况下,约束能确保 year
, month
和 day
参数匹配一个具体有效的模式,从而在稍后来减轻你的负担 .
概述
Grails基于Spring Web Flow项目来支持创建Web流(Flow)。一个Web流(Flow)就是一个会话,它跨越多个请求并保持着流(Flow)作用域的状态。 一个Web流(Flow)也定义了开始和结束状态。 .
Web流(Flow)无需HTTP
session,但作为替代,它将状态存储在序列化表单中,然后通过Grails来回传递的request参数中的执行流中的key进行还原。
这相比其他使用HttpSession来保存状态的应用来说更具有可扩展性,尤其是在内存和集群方面.
Web流(Flow)本质是高级的状态机,它管理着一个状态到下个状态"流"的执行。因为为你管理着状态,你就勿需担心用户在进入多步骤流(Flow)的操作(action)
,因为Web流(Flow)已经帮你管理了,因此Web流(Flow)在处理象网上购物、宾馆预定及任何多页面的工作流的应用具有出乎意料的简单.
创建流
创建一个流(Flow)只需简单的创建一个普通的Grails控制器(controller),然后添加一个以规约Flow
结尾的操作。例如:
class BookController {
def index = {
redirect(action:"shoppingCart")
}
def shoppingCartFlow = {
…
}
}
注意,当重定向或引用流(Flow)时,可以把它当做一个操作(action)而省略掉流(Flow)
前缀。换句话说,
上面流的操作(action)名为shoppingCart
.
如上所述,一个流(Flow)定义了开始和结束状态。一个开始状态是当用户第一次开始一个会话(或流(Flow))。Grails的开始流(Flow)是第一个带有代码块的方法调用。例如:
class BookController {
…
def shoppingCartFlow = {
showCart {
on("checkout").to "enterPersonalDetails"
on("continueShopping").to "displayCatalogue"
}
…
displayCatalogue {
redirect(controller:"catalogue", action:"show")
}
displayInvoice()
}
}
这里,showCart
节点是这个流的开始状态。
因为这个showCart状态并没有定义一个操作(action)或重定向,只被视为是一个视图状态。 通过规约,指向grails-app/views/book/shoppingCart/showCart.gsp
视图
.
注意,这不像正规的控制器(controller)操作(action),这个视图被存储于与其流名字匹配的grails-app/views/book/shoppingCart
目录中 .
shoppingCart
流(Flow)也可能拥有两个结束状态。第一个是displayCatalogue
,
执行外部重定向到另一个控制器(controller)和操作(action),从而结束流(Flow)。第二个是displayInvoice
是一个最终状态,因为它根本没有任何事件,
只是简单的渲染一个名为grails-app/views/book/shoppingCart/displayInvoice.gsp
的视图,并在同一时间终止流(Flow).
一旦一个流(Flow)结束,它只能从开始状态重新开始,对于showCart
不会来自任何其他状态.
视图状态
视图状态没有定义操作(action)
或redirect
。下面是一个视图状态示例:
enterPersonalDetails {
on("submit").to "enterShipping"
on("return").to "showCart"
}
它默认查找一个名为grails-app/views/book/shoppingCart/enterPersonalDetails.gsp
的视图。
注意,enterPersonalDetails
定义了两个事件:submit
和return
。视图负责触发(triggering)这些事件。假如你想让视图用于渲染,使用render方法来完成:
enterPersonalDetails {
render(view:"enterDetailsView")
on("submit").to "enterShipping"
on("return").to "showCart"
}
现在,它将查找grails-app/views/book/shoppingCart/enterDetailsView.gsp
。假如使用共享视图,视图参数以/
开头:
enterPersonalDetails {
render(view:"/shared/enterDetailsView")
on("submit").to "enterShipping"
on("return").to "showCart"
}
现在,它将查找 grails-app/views/shared/enterDetailsView.gsp
操作(Action)状态
操作(Action)状态只执行代码但不渲染任何视图。操作(Action)的结果被用于控制流(Flow)的切换。为了创建一个操作操作(Action)状态,你需要定义一个被用于执行的操作。
这通过调用action
方法实现并传递它的一个代码块来执行:
listBooks {
action {
[ bookList:Book.list() ]
}
on("success").to "showCatalogue"
on(Exception).to "handleError"
}
正如你看到的,一个操作看上去非常类似于一个控制器(controller)操作(action),实际上,假如你需要可以重用控制器(controller)操作(action)。
假如这个操作没有错误成功返回,success
事件将被触发。
在这里,返回一个map,它被视为"model"看待,并自动放置于流(flow)作用域.
此外,在上面的示例中也使用了下面的异常处理程序来处理错误:
on(Exception).to "handleError"
这使当流(Flow)切换到状态出现异常的情况下调用handleError
.
你可以编写与流(flow)请求上下文相互作用更复杂的操作(action):
processPurchaseOrder {
action {
def a = flow.address
def p = flow.person
def pd = flow.paymentDetails
def cartItems = flow.cartItems
flow.clear() def o = new Order(person:p, shippingAddress:a, paymentDetails:pd)
o.invoiceNumber = new Random().nextInt(9999999)
cartItems.each { o.addToItems(it) }
o.save()
[order:o]
}
on("error").to "confirmPurchase"
on(Exception).to "confirmPurchase"
on("success").to "displayInvoice"
}
这是一个更复杂的操作(action),用于收集所有来自流(flow)作用域信息,并创建一个Order
对象。
然后,把Order作为模型返回。这里值得注意的重要事情是与请求上下文和 "流(flow)作用域"的相互作用.
切换操作
另一种形式的操作(action)被称之为切换操作(action)。一旦一个event被触发,切换操作优先于状态切换被直接执行。普通的切换操作如下
:
enterPersonalDetails {
on("submit") {
log.trace "Going to enter shipping"
}.to "enterShipping"
on("return").to "showCart"
}
注意,我们是怎样传递一个代码块给submit
事件,它只是简单的记录这个切换。切换状态对于数据绑定与验证是非常有用的,将在后面部分涵盖.
为了执行流流从一个状态到下一个状态的
切换
,你需要一些方法来触发一个
event
,指出流流下一步该做什么。事件的触发可以来自于任何视图状态和操作状态.
来自于一个视图状态的触发事件
正如之前所讨论的,在早前代码列表内流的开始状态可能处理两个事件。一个checkout
和一个continueShopping
事件:
def shoppingCartFlow = {
showCart {
on("checkout").to "enterPersonalDetails"
on("continueShopping").to "displayCatalogue"
}
…
}
因为showCart
事件是一个视图状态,它会渲染 grails-app/book/shoppingCart/showCart.gsp
视图.
在视图内部,你需要拥有一个用于触发流(Flow)执行的组件.在一个表单中,这可使用submitButton标签:
<g:form action="shoppingCart">
<g:submitButton name="continueShopping" value="Continue Shopping"></g:submitButton>
<g:submitButton name="checkout" value="Checkout"></g:submitButton>
</g:form>
这个表格必须提交返回shoppingCart
流流。每个submitButton标签的name属性标示哪个事件将被触发。
假如,你没有表格,你同样可以用link标签来触发一个事件,如下:
<g:link action="shoppingCart" event="checkout" />
来自于一个操作(Action)的触发事件
为了触发来自于一个操作(action)
的一个事件,你需要调用一个方法。例如,这里内置的error()
和success()
方法。
下面的示例在切换操作中验证失败后触发error()
事件:
enterPersonalDetails {
on("submit") {
def p = new Person(params)
flow.person = p
if(!p.validate())return error()
}.to "enterShipping"
on("return").to "showCart"
}
在这种情况下,因为错误,切换操作将使流回到enterPersonalDetails
状态.
有了一种操作状态,你也能触发事件来重定向流:
shippingNeeded {
action {
if(params.shippingRequired) yes()
else no()
}
on("yes").to "enterShipping"
on("no").to "enterPayment"
}
作用域基础
在以前的示例中,你可能会注意到我们在“流作用域(flow scope)”中已经使用了一个特殊的流(flow)
来存储对象,在Grails中共有5种不同的作用域可供你使用
:
request
- 仅在当前的请求中存储对象
flash
- 仅在当前和下一请求中存储对象
flow
- 在工作流中存储对象,当流到达结束状态,移出这些对象
conversation
-
在会谈(conversation)中存储对象,包括根工作流和其下的子工作流
session
- 在用户会话(session)中存储对象
Grails的service类可以自动的定位web
flow的作用域,详细请参考Services .
此外从一个action中返回的模型映射(model
map)将会自动设置成flow范围,比如在一个转换(transition)的操作中,你可以象下面这样使用流(flow)
作用域
:
enterPersonalDetails {
on("submit") {
[person:new Person(params)]
}.to "enterShipping"
on("return").to "showCart"
}
要知道每一个状态总是创建一个新的请求,因此保存在request作用域中的对象在其随后的视图状态中不再有效,要想在状态之间传递对象
,需要使用除了request之外的其他作用域。此外还有注意,Web流(Flow)将 :
- 在状态转换的时候,会将对象从flash作用域移动到request作用域;
- 在渲染以前,将会合并flow和conversation作用域的对象到视图模型中(因此你不需要在视图中引用这些对象的时候,再包含一个作用域前缀了).
流(Flow)的作用域和序列化
当你将对象放到 flash
, flow
或conversation
作用域中的时候,要确保对象已经实现了java.io.Serializable
接口,否则将会报错。 这在domain类尤为显著,因为领域类通常在视图中渲染的时候被放到相应的作用域中,比如下面的领域类示例 :
class Book {
String title
}
为了能够让Book
类的实例可以放到流(flow)作用域中,你需要修改如下:
class Book implements Serializable {
String title
}
这也会影响到领域类中的关联和闭包,看下面示例:
class Book implements Serializable {
String title
Author author
}
此处如果Author
关联没有实现Serializable
,你同样也会得到一个错误。
此外在GORM events中使用的闭包比如onLoad
, onSave
等也会受到影响,
下例的领域类如果放到flow作用域中,将会产生一个错误:
class Book implements Serializable {
String title
def onLoad = {
println "I'm loading"
}
}
这是因为onLoad
事件中的代码块必能被序列化,要想避免这种错误,需要将所有的事件声明为transient
:
class Book implements Serializable {
String title
transient onLoad = {
println "I'm loading"
}
}
在
开始和结束状态
部分, 开始状态的第一个示例触发一个切换到
enterPersonalDetails
状态。这个状态渲染一个视图,并等待用户键入请求信息 :
enterPersonalDetails {
on("submit").to "enterShipping"
on("return").to "showCart"
}
一个视图包含一个带有两个提交按钮的表格,每个都触发提交事件或返回事件:
<g:form action="shoppingCart">
<g:submitButton name="submit" value="Continue"></g:submitButton>
<g:submitButton name="return" value="Back"></g:submitButton>
</g:form>
然而,怎么样捕捉被表格提交的信息?为了捕捉表格信息我们可以使用流切换操作:
enterPersonalDetails {
on("submit") {
flow.person = new Person(params)
!flow.person.validate() ? error() : success()
}.to "enterShipping"
on("return").to "showCart"
}
注意,我们是怎样执行来自请求参数的绑定,把Person
实体放置于流(flow)
作用域中。同样有趣的是,我们执行
验证,并在验证失败是调用error()
方法
.这个流(flow)的动机即停止切换并返回 enterPersonalDetails
视图,因此,有效的项通过user进入,否则,切换继续并转到enterShipping
state.
就像正规操作(action),流(flow)操作(action)也支持
命令对象概念,通过定义闭包的第一个参数 :
enterPersonalDetails {
on("submit") { PersonDetailsCommand cmd ->
flow.personDetails = cmd
!flow.personDetails.validate() ? error() : success()
}.to "enterShipping"
on("return").to "showCart"
}
Grails的Web Flow集成同样支持子流(subflows)。一个子流在一个流中就像一个流。拿下面search流作为示例:
def searchFlow = {
displaySearchForm {
on("submit").to "executeSearch"
}
executeSearch {
action {
[results:searchService.executeSearch(params.q)]
}
on("success").to "displayResults"
on("error").to "displaySearchForm"
}
displayResults {
on("searchDeeper").to "extendedSearch"
on("searchAgain").to "displaySearchForm"
}
extendedSearch {
subflow(extendedSearchFlow) // <--- extended search subflow
on("moreResults").to "displayMoreResults"
on("noResults").to "displayNoMoreResults"
}
displayMoreResults()
displayNoMoreResults()
}
它在extendedSearch
状态中引用了一个子流。子流完全是另一个流
:
def extendedSearchFlow = {
startExtendedSearch {
on("findMore").to "searchMore"
on("searchAgain").to "noResults"
}
searchMore {
action {
def results = searchService.deepSearch(ctx.conversation.query)
if(!results)return error()
conversation.extendedResults = results
}
on("success").to "moreResults"
on("error").to "noResults"
}
moreResults()
noResults()
}
注意,它是怎样把extendedResults
放置于会话范围的。这个范围不同于流范围,因为它允许你横跨整个会话而不只是这个流。
同样注意结束状态(每个子流的 moreResults
或 noResults
在主流中触发事件
:
extendedSearch {
subflow(extendedSearchFlow) // <--- extended search subflow
on("moreResults").to "displayMoreResults"
on("noResults").to "displayNoMoreResults"
}
尽管Grails支持良好的细粒度控制器(controller),但只对少数控制器(controller)的应用时非常有用,当管理大型应用时就会变得很困难。
另一方面,过滤器能横跨一群控制器(controller),一个URI空间或一个具体的操作(action)。
过滤器对插件更容易并能保证彻底的分离主要控制器(controller)逻辑,有利于所有像安全,日志等等这样的横切关注点 .
为了创建一个过滤器,可在
grails-app/conf
下创建一个以规约
Filters
结尾的类。在这个类中,定义一个名为
filters
的代码块,它包含了过滤器的定义 :
class ExampleFilters {
def filters = {
// your filters here
}
}
每个在filters
块中定义的过滤器(Filters)拥有一个名字和一个
作用域。名字是方法的名字,作用域使用命名参数来定义。例如,假如你需要定义一个应用于所有控制器(controller)和操作(action)的过滤器(Filters)可以使用通配符 :
sampleFilter(controller:'*', action:'*') {
// interceptor definitions
}
过滤器的作用域可以是下面之一:
- 具有通配符的一个控制器(controller)和/或操作(action)名字对
- 具有Ant路径匹配语法的一个URI
过滤器的一些示例包括:
- 所有控制器(controller)和操作(action)
all(controller:'*', action:'*') {}
justBook(controller:'book', action:'*') {}
someURIs(uri:'/book/**') {}
另外,这个次序决定了你所定义的过滤器的执行次序.
在过滤器的主体内,你可以定义下列过滤器(Filters)的拦截器类型之一:
before
- 操作(Action)之前执行.
返回false来指示后续的控制器(controller)和操作(action)不会被执行
after
- 操作(Action)之后执行. 获取第一参数作为视图模型
afterView
- 视图渲染之后执行
例如,为实现普通身份验证,可以定义如下过滤器(Filters):
class SecurityFilters {
def filters = {
loginCheck(controller:'*', action:'*') {
before = {
if(!session.user && !actionName.equals('login')) {
redirect(action:'login')
return false
}
} }
}
}
这里的loginCheck
过滤器(Filters)使用一个before
拦截器来执行代码块,
检查是否一个用户在session内,假如不是,重定向到login操作(action)。注意,如何返回false确保操作(action)本身不被执行 .
过滤器支持所有在
控制器(controllers)
和
标签库
中可用的属性,附加application context :
不过,过滤器只支持用于控制器(controller)和标签库方法的子集 。这些包括:
Ajax代表异步Javascript与XML,它是转向富web应用程序的驱动力.
这些类型的应用程序,通常更适合于像Ruby和
Groovy语言所写的敏捷,动态框架,Grails通过它的Ajax标签库提供支持构建Ajax应用程序.
它们完整的列表可以参看标签库参考.
Grails默认装载Prototype
库,但通过Plug-in
系统,可以提供对Dojo,
Yahoo UI
和
Google
Web Toolkit
等其他框架的支持.
这部分涵盖Grails对Prototype的支持。你需要在页面的<head>
标签内添加这样一行就可以开始了
:
<g:javascript library="prototype" />
这里使用javascript标签自动插入Prototype正确位置的引用。假如你同样需要Scriptaculous
,你可以如下这样做为替换 :
<g:javascript library="scriptaculous" />
远程内容可以通过多种方式加载,最常使用的方法是通过
remoteLink
标签。 这个标签允许创建的HTML锚标记执行一个异步请求,并在一个元素中随意设置响应。用这个简单方式创建的远程链接就像这样 :
<g:remoteLink action="delete" id="1">Delete Book</g:remoteLink>
上面的连接发送一个异步请求给当前id为1
的控制器的delete
操作
.
这真是太棒了,但通常你想提供一些事情发生的反馈信息给用户:
def delete = {
def b = Book.get( params.id )
b.delete()
render "Book ${b.id} was deleted"
}
GSP代码:
<div id="message"></div>
<g:remoteLink action="delete" id="1" update="message">Delete Book</g:remoteLink>
上面的示例将调用这个操作并设置message
div
的响应内容为"Book
1 was deleted"
。这通过标签上的update
属性来完成,它同样可以获取一个map来指出在失败时什么被更新
:
<div id="message"></div>
<div id="error"></div>
<g:remoteLink action="delete" id="1"
update="[success:'message',failure:'error']">Delete Book</g:remoteLink>
这里,error
div在请求失败时被更新.
,一个HTML form也可以异步被提交通过以下两种方式之一。第一个,使用
formRemote
标签,它和
remoteLink
标签有类似的属性 :
<g:formRemote url="[controller:'book',action:'delete']" update="[success:'message',failure:'error']">
<input type="hidden" name="id" value="1" />
<input type="submit" value="Delete Book!" />
</g:formRemote >
或者作为选择可以使用submitToRemote来创建一个提交按钮。它允许一些按钮远程提交而一些不依赖操作
:
<form action="delete">
<input type="hidden" name="id" value="1" />
<g:submitToRemote action="delete" update="[success:'message',failure:'error']" />
</form>
某些事件的发生会调用特定的javascript。所有以"on"开头的事件,在适当的时候允许你反馈信息给用户,或采取其他行为:
<g:remoteLink action="show"
id="1"
update="success"
onLoading="showProgress()"
onComplete="hideProgress()">Show Book 1</g:remoteLink>
上述代码将执行"showProgress()"函数来显示一个进度条或者其他适当的展示,其他的事件还包括 :
onSuccess
- 成功时调用的javascript函数
onFailure
- 失败时调用的javascript函数
on_ERROR_CODE
- 处理指定的错误代码时调用的javascript函数 (例如
on404="alert('not found!')")
onUninitialized
- 一个ajax引擎初始化失败时调用的javascript函数
onLoading
- 当远程函数加载响应时调用的javascript函数
onLoaded
- 当远程函数加载完响应时调用的javascript函数
onComplete
- 当远程函数完成(包括任何更新)时调用的javascript函数
假如你需要引用XmlHttpRequest
对象,你可以使用隐式的event参数e
获取它
:
<g:javascript>
function fireMe(e) {
alert("XmlHttpRequest = " + e)
}
}
</g:javascript>
<g:remoteLink action="example"
update="success"
onSuccess="fireMe(e)">Ajax Link</g:remoteLink>
Grails把
Dojo
作为一种外部插件来支持Grails的特性。在终端窗口,进入你项目的根目录键入下列命令来安装插件 :
grails install-plugin dojo
将下载Dojo最新的支持版本,并安装到你的Grails项目中。完成上面的步骤后,你可以在你页面的顶部添加下列引用:
<g:javascript library="dojo" />
现在,所有像remoteLink, formRemote 和 submitToRemote标签都可以和Dojo进行远程处理工作 .
Grails同样支持
Google
Web Toolkit
特性,插件的全面
文档
可以在Grails wiki中找到 .
虽然Ajax特性X为XML,但通常可以分解成许多不同方式执行Ajax:
- 内容为中心的 Ajax - 只不过是使用远程调用的HTML结果来更新页面
- 数据为中心的Ajax - 实际上是发送一个来自于服务器端的XML或JSON,通过编程更新页面
- 脚本为中心的 Ajax - 服务器端发送的Javascript流在运行中被赋值
在Ajax部分中的更多的示例涵盖了内容为中心的
Ajax在什么地方更新页面,但同样你可能使用数据为中心的Ajax或脚本为中心的 Ajax。这份指南涵盖了不同风格的Ajax .
内容为中心的Ajax
作为概括,内容为中心的 Ajax涉及从服务器端发送一些HTML返回和通过使用render方法来渲染模板
:
def showBook = {
def b = Book.get(params.id) render(template:"bookTemplate", model:[book:b])
}
在客户端调用这个会涉及到remoteLink标签的使用 :
<g:remoteLink action="showBook" id="${book.id}" update="book${book.id}">Update Book</g:remoteLink>
<div id="book${book.id}">
</div>
数据为中心的Ajax与JSON
数据为中心的Ajax通常涉及到客户端响应的赋值和编程化更新。Grails中的JSON响应,通常使用Grails的JSON
marshaling能力 :
import grails.converters.*def showBook = {
def b = Book.get(params.id)
render b as JSON
}
然后,在客户端使用一个Ajax事件处理解析这个进入的JSON请求:
<g:javascript>
function updateBook(e) {
var book = eval("("+e.responseText+")") // evaluate the JSON
$("book"+book.id+"_title").innerHTML = book.title
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
<div id="${bookId}_title">The Stand</div>
</div>
数据为中心的Ajax与XML
在服务器端使用XML同样普遍:
import grails.converters.*def showBook = {
def b = Book.get(params.id)
render b as XML
}
不过,因为涉及到DOM,客户变得更复杂:
<g:javascript>
function updateBook(e) {
var xml = e.responseXML
var id = xml.getElementsByTagName("book").getAttribute("id")
$("book"+id+"_title")=xml.getElementsByTagName("title")[0].textContent
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
<div id="${bookId}_title">The Stand</div>
</div>
脚本为中心的Ajax与JavaScript
脚本为中心的 Ajax涉及实际返回的Javascript在客户端被赋值。这样的示例见下表:
def showBook = {
def b = Book.get(params.id) response.contentType = "text/javascript"
String title = b.title.encodeAsJavascript()
render "$('book${b.id}_title')='${title}'"
}
要记住的重要事情是,设置contentType
为text/javascript
。如果在客户端使用Prototype,由于设置了contentType
,
返回的Javascript将自动被赋值.
很明显,在这种情况下,它是关键性的,你有一个一致的client-sideAPI,
因此,你不想客户端的改变破坏服务器端。这就是Rails有些像RJS的理由之一。
虽然,Grails当前没有像RJS的一个特性,但动态Dynamic JavaScript Plug-in插件提供了类似的能力.
Grails已经内置支持内容协商通过使用任意HTTP
Accept
报头
,一种明确格式请求参数或URI映射的扩展.
配置Mime类型
在你开始处理内容协商之前,你必须告诉Grails希望支持什么样的内容类型。 默认情况下,grails-app/conf/Config.groovy
内使用
grails.mime.types
设置来配置若干不同的内容类型 :
grails.mime.types = [ xml: ['text/xml', 'application/xml'],
text: 'text-plain',
js: 'text/javascript',
rss: 'application/rss+xml',
atom: 'application/atom+xml',
css: 'text/css',
cvs: 'text/csv',
all: '*/*',
json: 'text/json',
html: ['text/html','application/xhtml+xml']
]
上面的小块配置,允许Grails检查把包含 'text/xml' 或
'application/xml' 媒体类型的一个请求的格式只当做 'xml'看待,你可以添加你自己的类型通过简单的添加条目到 map中.
内容协商使用Accept报头
每个进入的HTTP请求都有个指定的Accept报头,它定义了什么样的媒体类型(或 mime
类型)客户端能"接受"。在老式浏览器中通常是 :
这意味着任何事物.不过在新生浏览器中,所有东西一起像这样发送更有用(一个FirefoxAccept
报头示例)
:
text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Grails解析这个进入的格式,并添加一个property
给request对象,用于描叙首选的请求格式。对于上述示例下列的断言会通过
:
assert 'html' == request.format
为什么?这个text/html
媒体类型拥有最高"质量"等级0.9,因此,具有最高优先权。如前所述,假如你有一老式浏览器结果会稍微不同
:
assert 'all' == request.format
在这种情况下,'all'可能的格式会被客户端接受。为了处理来自控制器(Controllers)不同类型的请求,你可以使用withFormat方法,它的行为被当作switch表达式
:
import grails.converters.*class BookController {
def books
def list = {
this.books = Book.list()
withFormat {
html bookList:books
js { render "alert('hello')" }
xml { render books as XML }
}
}
}
当Grails只执行 html()
调用并且首选的格式是html
时会发生什么。它只是让
Grails寻找每个名为grails-app/views/books/list.html.gsp
或grails-app/views/books/list.gsp
的视图。
如果格式是xml
,那么,闭包会被调用,XML响应会被渲染 .
我们怎样处理'all'格式?只需在withFormat
代码块中简单指定content-types,以便,无论你想要的哪个都会被首先执行。
因此,在上面示例中的"all" 将触发html
处理
.
当使用withFormat时确保它在控制器(controller)操作(action)中最后一个被调用,因为withFormat
方法的返回值用来决定操作(action)下一步做什么.
内容协商与格式化请求参数
如果请求头的内容跟你的不一致,通过指定一个format
的请求参数覆盖这个格式
:
你同样可以在URL
Mappings定义中定义这个参数 :
"/book/list"(controller:"book", action:"list") {
format = "xml"
}
内容协商与URI扩展
Grails同样可以通过URI扩展支持内容协商。例如,给定下列URI:
Grails将剔除扩展并映射到/book/list
作为替代,同时,基于这个扩展把内容格式化为xml
。
这个行为是默认允许的,那么,假如你希望关闭它, 你必须把grails-app/conf/Config.groovy
下的grails.mime.file.extensions
属性设置为false
:
grails.mime.file.extensions = false
测试内容协商
为了在一个综合测试中测试内容协商(参见 测试部分)你可以操作每个进入的请求包头
:
void testJavascriptOutput() {
def controller = new TestController()
controller.request.addHeader "Accept", "text/javascript, text/html, application/xml, text/xml, */*"
controller.testAction()
assertEquals "alert('hello')", controller.response.contentAsString
}
或者你可以设置格式化参数来实现类似的效果:
void testJavascriptOutput() {
def controller = new TestController()
controller.params.format = 'js' controller.testAction()
assertEquals "alert('hello')", controller.response.contentAsString
}